# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: taekwon(originally from coinone_v2/kr_rest/private_client.py)

import base64
import datetime
import enum
import functools
import hashlib
import hmac
import json
import logging
import time
import urllib.parse

import requests
import requests.auth
import sys

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.util.order_checker import OrderAmountChecker
from coin.exchange.base.rest.request_nonce_manager import RequestNonceManager
from coin.exchange.util.async_http_client import AsyncHttpClient
from tornado.ioloop import IOLoop


def is_success_response_msg(msg):
  return not 'errormsg' in msg


class GopaxOrderSide(enum.Enum):
  BUY = 'buy'
  SELL = 'sell'


GOPAX_API_HOST = 'api.gopax.co.kr'


class GopaxAuth(requests.auth.AuthBase):
  def __init__(self, key_file, rest_request_gap_sec=None):
    self._key = AuthKey.from_file(key_file) if key_file else None
    self._token = None
    self._auth_value = None
    self._nonce_manager = RequestNonceManager(request_gap_sec=rest_request_gap_sec)

  def __call__(self, req: requests.PreparedRequest):
    params = {}
    body = ''
    if req.body:
      body = req.body.decode('utf8')
      params = json.loads(body)
    nonce = str(self._nonce_manager.get_nonce())
    path = urllib.parse.urlparse(req.url).path
    hash_str = nonce + req.method + path + body
    hash_str = hash_str.encode('utf8')
    signature = hmac.new(base64.b64decode(self._key.secret_key), hash_str, hashlib.sha512)
    headers = {
        'API-KEY': self._key.access_key,
        'SIGNATURE': base64.b64encode(signature.digest()),
        'NONCE': nonce,
    }
    req.headers.update(headers)
    req.prepare_body(data=None, files=None, json=params)
    return req


class GopaxAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
      rest_request_gap_sec=None,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = GopaxAuth(key_file, rest_request_gap_sec)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or GOPAX_API_HOST)

  def query(self, method, path, params=None):
    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(
        method=method,
        url=url,
        json=params,
        auth=self._auth,
        timeout=self._timeout,
    )
    return future


class GopaxSyncQueryer(object):
  def __init__(self,
               key_file,
               *,
               api_host=None,
               disable_ssl=False,
               rest_request_gap_sec=None,
               timeout=10):
    self._timeout = timeout
    self._auth = GopaxAuth(key_file, rest_request_gap_sec)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or GOPAX_API_HOST)
    self._logger = logging.getLogger(__name__)
    self.rate_limit = RateLimiter(20, 1.0)

  def auth_request(self, method, path, params):
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.request(method, url, json=params, auth=self._auth, timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except json.JSONDecodeError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


class GopaxNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
      rest_request_gap_sec=None,
  ):
    api_host = api_host or GOPAX_API_HOST
    if use_async:
      self._auth_queryer = GopaxAsyncQueryer(
          key_file,
          api_host=api_host,
          disable_ssl=disable_ssl,
          timeout=timeout,
          max_connections=max_connections,
          rest_request_gap_sec=rest_request_gap_sec,
      )
    else:
      self._auth_queryer = GopaxSyncQueryer(
          key_file,
          api_host=api_host,
          timeout=timeout,
          rest_request_gap_sec=rest_request_gap_sec,
      )
    self.order_amount_checker = OrderAmountChecker("KRW", 500, 50000000)

  def _query(self, method, path, params=None):
    return self._auth_queryer.query(method, path, params=params)

  def query_account_balance(self):
    method = 'GET'
    path = 'balances'
    update = self._query(method, path)
    return update

  def cancel_order(self, *, order_id: str):
    method = 'DELETE'
    path = 'orders/{}'.format(order_id)
    update = self._query(method, path)
    return update

  def place_limit_order(self, *, currency, side, price, qty):
    method = 'POST'
    path = 'orders'
    params = {
        'type': 'limit',
        'side': side,
        'tradingPairName': currency,
        'price': price,
        'amount': qty,
    }
    self.order_amount_checker.check_amount('KRW', float(price), float(qty))
    update = self._query(method, path, params)
    return update

  def query_order_info(self, order_id):
    method = 'GET'
    path = 'orders/{}'.format(order_id)
    update = self._query(method, path)
    return update

  def query_orders(self, currency):
    method = 'GET'
    path = 'orders'
    params = {'trading-pair-name': currency, 'status': 'completed'}
    update = self._query(method, path, params)
    return update

  def query_trades(self, currency):
    method = 'GET'
    path = 'trades'
    params = {'trading-pair-name': currency}
    update = self._query(method, path, params)
    return update


class TestGopaxNativePrivateClient(object):
  def __init__(self, key_filepath, use_async=True, ioloop=None):
    self._key_filepath = key_filepath
    self._use_async = use_async
    self._ioloop = ioloop
    self._private_client = None
    self._scheduler = None
    self._logger = logging.getLogger(__name__)

  def start(self):
    self._private_client = GopaxNativePrivateClient(key_file=self._key_filepath,
                                                    use_async=self._use_async)

  def on_submit_response(self, orders, future):
    try:
      res = future.result()
      print(res.json())
      print(orders)
    except Exception as e:
      print(e)

  def order(self):
    future = self._private_client.place_limit_order(currency='EOS-KRW',
                                                    side='sell',
                                                    price=4733.0,
                                                    qty=1.5)
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order1']))

  def cancel_order(self, order_id):
    future = self._private_client.cancel_order(order_id=order_id)
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order2']))

  def query_account_balance(self):
    future = self._private_client.query_account_balance()
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order4']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_account_balance)

  def query_orders(self):
    future = self._private_client.query_orders('EOS-KRW')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order6']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_orders)

  def query_trades(self):
    future = self._private_client.query_trades('EOS-KRW')
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order7']))
    self._ioloop.add_timeout(datetime.timedelta(seconds=10), self.query_trades)

  def query_order_info(self, order_id):
    future = self._private_client.query_order_info(order_id)
    self._ioloop.add_future(future, functools.partial(self.on_submit_response, ['order8']))

  def query_account_balance_sync(self):
    print("Sync.")
    print(self._private_client.query_account_balance())

  def query_trades_sync(self):
    print("Sync.")
    print(self._private_client.query_trades('EOS-KRW'))


def main(argv):
  key_filepath = '/home/taekwon/gopax_api_key.json'

  ioloop = IOLoop.current()
  test_client_async = TestGopaxNativePrivateClient(key_filepath, use_async=True, ioloop=ioloop)
  test_client_async.start()
  test_client_async.query_account_balance()
  # test_client_async.order()
  # test_client_async.cancel_order('2106559324')
  # test_client_async.query_open_orders()
  test_client_async.query_orders()
  # test_client_async.query_order_info('211057945')
  test_client_async.query_trades()

  test_client_sync = TestGopaxNativePrivateClient(key_filepath, use_async=False, ioloop=ioloop)
  test_client_sync.start()
  test_client_sync.query_account_balance_sync()
  test_client_sync.query_trades_sync()
  ioloop.start()


if __name__ == '__main__':
  logging.basicConfig(level='INFO', format='%(levelname)s %(asctime)s %(name)s] %(message)s')
  sys.exit(main(sys.argv))
